function gameEngine(mode) {
	this.gameMode = mode;
	this.gameObjects = {};
	this.selected = [];
	this.fps = 60;
	this.newGoId = 0;
	this.player = null;
	this.players = {};
    this.creditsTime = 5;
    this.creditsTimer = this.creditsTime;
	this.frameCounter = 0;
	this.awaitingOrders = false;
	this.networkFramesPerSecond = 12;
	// Moving more units than this breaks the network.
	// TODO: fix the network
	this.unitSelectionCap = 32;

	this.map = new map(64, 64);

	this.gameObjectFactory = new gameObjectFactory();
	this.graphicsEngine = new graphicsEngine(this.map, this.gameObjectFactory);
	this.collisionEngine = new collisionEngine();
	this.networkingEngine = new networkingEngine();

	if(mode !== gameMode.multiPlayer) {
		var initialGameState = String.fromCharCode(0);
		for(var i = 0; i < 2; i++) {
			initialGameState += String.fromCharCode(i) + String.fromCharCode(50) + String.fromCharCode(1-i);
		}
		this.initializeGameState(initialGameState);
	}
}

gameEngine.prototype.recalculateSelected = function(startX, startY, endX, endY) {
	startX += this.graphicsEngine.cameraX;
	startY += this.graphicsEngine.cameraY;
	endX += this.graphicsEngine.cameraX;
	endY += this.graphicsEngine.cameraY;
    this.selected.length = 0;
    if(startX === endX && startY === endY) {
        var go = this.map.tiles[Math.floor(startX / this.map.tileSize)][Math.floor(startY / this.map.tileSize)].occupiedBy;
        if(go !== null) {
            this.selected.push(go);
            return;
        }
    }
	var buildings = [];
	var units = [];
    for(var goId in this.gameObjects){
        var go = this.gameObjects[goId];
        var x = go.x + go.width / 2;
        var y = go.y + go.height / 2;
        if(go.player === this.player && x > startX - this.map.tileSize/2 && y > startY - this.map.tileSize/2 && x < endX + this.map.tileSize/2 && y < endY + this.map.tileSize/2) {
			if(go.moveSpeed > 0) {
				units.push(go);
			}
			else {
				buildings.push(go);
			}
        }
		if(units.length >= this.unitSelectionCap || buildings.length >= this.unitSelectionCap) {
			break;
		}
    }
	if(units.length === 0) {
		this.selected = buildings;
	}
	else {
		this.selected = units;
	}
}

gameEngine.prototype.buildFrom = function(gameObjectType) {
    var found = false;
    for(var i = 0; i < this.selected.length; i++) {
		var go = this.selected[i];
        if(go.canBuild && go.player === this.player && go.canBuild.indexOf(gameObjectType) !== -1) {
            return go;
        }
    }
    if(!found) return null;
}

gameEngine.prototype.createGameObject = function(template, player, tileX, tileY) {
	var go = this.gameObjectFactory.createGameObject(this.newGoId, player, template, tileX, tileY, this.graphicsEngine.images, this.map);
    go.destroyed.push({caller: this, fn: this.removeGameObject});
    this.gameObjects[go.id] = go;
    this.collisionEngine.addGameObject(go);

    this.newGoId++;
}

gameEngine.prototype.canBuildStructure = function(template, player, tileX, tileY) {
	for(var i = tileX - 4; i < tileX + template.width / this.map.tileSize + 4; i++) {
		for(var j = tileY - 4; j < tileY + template.height / this.map.tileSize + 4; j++) {
			if(this.map.tileInBounds(i, j)) {
				var go = this.map.tiles[i][j].occupiedBy;
				if(go !== null && go.player === player && go.moveSpeed === 0) {
					return true;
				}
			}
		}
	}
	return false;
}

gameEngine.prototype.removeGameObject = function(self, gameObject) {
	delete self.gameObjects[gameObject.id];
    self.selected.remove(gameObject);
	self.collisionEngine.removeGameObject(gameObject);
}

//self is a hack to fix "this" scope issue
gameEngine.prototype.loop = function(self) {
	if(self.frameCounter % (self.fps / self.networkFramesPerSecond) === 0) {
		var networkFrame = self.frameCounter / (self.fps / self.networkFramesPerSecond);
		if(!self.awaitingOrders) {
			self.networkingEngine.dispatchOrders(networkFrame + 2, self.player.id, self.gameMode);
			self.awaitingOrders = true;
		}

		var orders = self.networkingEngine.incomingOrders[0];
		if(orders !== undefined && networkFrame === orders.charCodeAt(0)) {
			self.networkingEngine.incomingOrders.shift();
			if(!self.gameMode === gameMode.replay) {
				localStorage.setItem('lastReplay'+networkFrame, orders);
			}
			self.interpretOrders(orders.substr(1));
			self.awaitingOrders = false;
		}
		else {
			//console.log('awaiting orders');
			return;
		}
	}
    
    $('#credits').html("Credits: " + self.player.credits);
    if(self.creditsTimer <= 0) {
        for(var p in self.players) {
			var player = self.players[p];
			var creditsPerTurn = 0;
			for(var goId in self.gameObjects) {
				var go = self.gameObjects[goId];
				if(go.player === player) {
					creditsPerTurn+=go.profit;
				}
			}
            player.credits+=creditsPerTurn;
            self.creditsTimer = self.creditsTime;
        }
    }
    else {
        self.creditsTimer -= 1000 / self.fps / 1000;
    }

	for(var goId in self.gameObjects){
		var go = self.gameObjects[goId];
		go.move();
	}

	self.collisionEngine.update();

	for(var goId in self.gameObjects){
		var go = self.gameObjects[goId];
		go.attack(self.collisionEngine);
		go.build();
	}

	self.graphicsEngine.draw(self.gameObjects, self.selected);

	self.frameCounter++;
}

gameEngine.prototype.initializeGameState = function(state) {
	for(var i = 1; i < state.length; i+=3) {
		this.players[state.charCodeAt(i)] = new player(state.charCodeAt(i), state.charCodeAt(i+1), state.charCodeAt(i+2));
	}
	this.player = this.players[state.charCodeAt(0)];
    
    //battle test
	/*for(var i = 0; i < 10; i++) {
		for(var j = 0; j < 10; j++) {
			this.createGameObject(this.gameObjectFactory.lizardManTemplate, 0, 1 + j, i + 1);
			this.createGameObject(this.gameObjectFactory.lizardManTemplate, 1, 20 + j, i + 1);
		}
	}*/
    
    this.createGameObject(this.gameObjectFactory.bioSphereTemplate, this.players[state.charCodeAt(1)], 11, 8);
    this.createGameObject(this.gameObjectFactory.lizardTempleTemplate, this.players[state.charCodeAt(4)], this.map.width - 14, this.map.width - 11);
	
	if(this.player.id === 1) {
		this.graphicsEngine.putCamera(this.map.width * this.map.tileSize, this.map.height * this.map.tileSize);
	}
    
	var networkFrame = this.frameCounter / (this.fps / this.networkFramesPerSecond);
	this.networkingEngine.dispatchOrders(networkFrame, this.player.id, this.gameMode);
	this.networkingEngine.dispatchOrders(networkFrame+1, this.player.id, this.gameMode);

	this.graphicsEngine.setBuildButtons(this.gameObjectFactory, this.player);

	setInterval(this.loop, 1000/this.fps, this);
}

gameEngine.prototype.interpretOrders = function(orders) {
	var index = 0;
	while(index < orders.length) {
		var player = this.players[orders.charCodeAt(index)];
		index++;
		var numOrders = orders.charCodeAt(index);
		index++
		var orderCount = 0;
		while(orderCount < numOrders) {
			switch(orders.charCodeAt(index)) {
				case orderTypes.buildStructure:
					index++;
					//template, playerId, tileX, tileY
					var template = this.gameObjectFactory[this.gameObjectFactory.findGameObjectNameById(orders.charCodeAt(index))+'Template'];
					var playerId = orders.charCodeAt(index+1);
					var tileX = orders.charCodeAt(index+2);
					var tileY = orders.charCodeAt(index+3);
					if(!this.map.tileInBounds(tileX, tileY) || !this.map.tileInBounds(tileX + template.width / this.map.tileSize - 1, tileY + template.height / this.map.tileSize - 1)) {
						return;
					}
					for(var i = tileX; i < tileX + template.width / this.map.tileSize; i++) {
						for(var j = tileY; j < tileY + template.height / this.map.tileSize; j++) {
							if(!this.map.tiles[i][j].passable) {
								return;
							}
						}
					}
					if(template.moveSpeed === 0) {
						if(!this.canBuildStructure(template, this.players[playerId], tileX, tileY)) {
							 return;
						}
					}
					if(template.price <= this.players[playerId].credits) {
						this.players[playerId].credits -= template.price;
                    	this.createGameObject(template, this.players[playerId], tileX, tileY);
					}
                    index += 4;
					break;



				case orderTypes.buildUnit:
					index++;
					var typeName = this.gameObjectFactory.findGameObjectNameById(orders.charCodeAt(index+1));
                    this.gameObjects[orders.charCodeAt(index)].queueUnit(this.gameObjectFactory[typeName+'Template']);
                    index += 2;
					break;



				case orderTypes.cancelUnit:
					index++;
					var cancelFrom = this.gameObjects[orders.charCodeAt(index)];
					index++;
					var queueIndex = orders.charCodeAt(index);
					index++;
					cancelFrom.cancelUnit(queueIndex);
					break;



				case orderTypes.move:
					index++;
					var x = orders.charCodeAt(index);
					index++;
					var y = orders.charCodeAt(index);
					index++
					var unitsToMove = orders.charCodeAt(index);
					index++;
					for(var i = 0; i < unitsToMove; i++) {
						var go = this.gameObjects[orders.charCodeAt(index+i)];
						if(go !== undefined && go.player === player) {
							go.moveTo(x, y);
						}
					}
					index+=unitsToMove;
					break;



				case orderTypes.attack:
					index++;
					break;



			}
			orderCount++;
		}
	}
}

var gameMode = {
	singlePlayer: 0,
	multiPlayer: 1,
	replay: 2
}
